fix(spec): answer an axis-silent key with both ends of the range, not the cheaper-spelled one - #19318
Conversation
… a range `findClosestMatches` ranks by edit distance alone, so on a shape declaring both ends of a range an axis-silent key is answered with whichever end is spelled more cheaply. `dateField` lands on `endDateField` (distance 3, inside a 9-char key's budget of 3) while `startDateField` sits at 5, structurally unreachable — and the suggested key PARSES, so an author who follows the protocol's own correction binds the wrong end of the event and is told nothing. Screen the fallback's answer: when the candidate carries an axis token the authored key does not and the shape also declares its opposite-pole sibling, replace the rename with a prescription naming BOTH ends. Declared `aliases` entries are never screened. The accepted key set does not move. Claude-Session: https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2 Co-authored-by: Claude <noreply@anthropic.com>
…s about An axis row no shape declares both ends of can never match, and a row nothing can match reads as coverage of a trap this protocol does not have — the same dead-entry shape `alias-integrity.test.ts` already refuses for `aliases` and `guidance`. Judge it in the same walk, with a lit control so an empty verdict is a reading rather than a search that matched nothing. Claude-Session: https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2 Co-authored-by: Claude <noreply@anthropic.com>
…ggester-opposite-sibling
Clause-②: no — the accepted key set does not move; only the refusal's suggestion text does. Claude-Session: https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2 Co-authored-by: Claude <noreply@anthropic.com>
Adding a module to the import graph reshuffles TypeScript's declaration-emit order for enum members: 19 declarations across four shards move `read`, `edit` or `update` to a different position. Those three lines are the WHOLE diff — nothing is added, removed or retyped, and a structural object type does not depend on member order. Controlled, not assumed: with `polarity-axes.ts` removed from the graph and `suggestions.zod.ts` restored to the merge base, a fresh build reports "declaration text unchanged (17 entry points, 5364 declarations)". The reshuffle is this branch's, so the artifact is regenerated here rather than left for the next card to find. Claude-Session: https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2 Co-authored-by: Claude <noreply@anthropic.com>
…ggester-opposite-sibling
📓 Docs Drift CheckThis PR changes 1 package(s): 24 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 1 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 136 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 8b3dbbfd047485c47090f6eb16034a6de344da66 && git checkout 8b3dbbfd047485c47090f6eb16034a6de344da66
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin c334ba0f3a6dc2a107ffca06bc28fb27775cba28 4e2406481e0153723dae7fc91293a22d85de34c9 && git checkout -B drift-repro c334ba0f3a6dc2a107ffca06bc28fb27775cba28 && git merge --no-ff 4e2406481e0153723dae7fc91293a22d85de34c9
node scripts/docs-audit/affected-docs.mjs --json c334ba0f3a6dc2a107ffca06bc28fb27775cba28
|
Contract reviewServed-tier: 75/75 Served-tier reading: the reviewer's own subagent transcript file, per-request harness Sources read: card #18572 body (retracted mechanism), retraction 5707586601, re-grading 5747478673 (the live scope and the stop condition), os-dev-report 5749401816; PR body, 9 file rows, 6 commits, 35 check runs at head; the tree at the head and at the merge base ① Derived judgments
Tests at head: ② Semver levelpatch — matches the changeset ③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code Generated by Claude Code |
…easured 389 The figure 136 does not reproduce. It came from a one-off census script whose forcing walk was weaker than the audit's in four ways: it imported only `*.zod.ts` plus `index.ts` (225 of 1012 modules under `packages/spec/src`), returned early on function-valued schemas, capped its walk at depth 12 instead of 40, and never invoked a deferred error map, so surfaces that register on first use never registered at all. Re-measured with `alias-integrity.test.ts`'s own instrument copied verbatim: 389 unique surfaces, 421 raw registrations, 388 distinct surface strings. Those are three different facts, so each citation now says which one it quotes. The conclusion is unchanged and was re-derived over the larger population: the same four fuzzy instances, the same single declared-alias row left alone. The attestation table gains the pairs the undercount had hidden (min/max 11 rather than 3, input/output and read/write 2 each). Comment-only in the source: `check:api-surface-declarations` reports "declaration text unchanged (17 entry points, 5364 declarations)" and `check:generated` all 16 artifacts up to date. Claude-Session: https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2 Co-authored-by: Claude <noreply@anthropic.com>
⛔ 达档契约复核暂时起不来 —— 本 PR 在队列外等档,⛔ 不降档、⛔ 不绕
事实本 PR 的 head 在 2026-09-20T13:05Z 前后从 本席据此起了一次增量达档复核。它没能开始,平台原因逐字:
处置,按章程原文
⇒ 本席:⛔ 不在别的档位跑这次复核;⛔ 不援引「已在旧 head 上 PASS」把新 head 当作已复核;⛔ 不动任何标签。本 PR 停在这里等档。
⭐ 这次耗尽的成因,如实记下本席今日按维护者当日指令「后续优先派需要 fable 的任务」执行,本轮先后起了五次达档复核(#19024、#19296、#19313、#19318、#19302),这正是额度耗尽的直接原因。⛔ 这不是对该指令的异议 —— 指令被执行了,代价随之出现,而代价该被看见而不是被绕过。 Generated by Claude Code |
…ggester-opposite-sibling Resolves 4 delete/modify conflicts in packages/spec/api-surface-declarations/ (api.txt, kernel.txt, root.txt, security.txt) by taking main's deletion: #19024 retired the whole declaration-text snapshot mechanism (script, package.json scripts, check:generated gate, files) and this branch had only modified those now-retired files. Verified check:api-surface and check:generated still pass, and that the branch's new polarity-axes.ts exports are not part of the public barrel (not re-exported from shared/index.ts), so no export-recording artifact needs a change. Co-Authored-By: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Isolated reviewer, round 3 (the head moved twice since the PASS at ① Derived judgments
Text only. Not a widening; not a narrowing. RIGHT.
② Semver levelpatch, and the changeset agrees ( ③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
Landing provenance — contract review PASS, landing preconditions measured, 2026-09-20T14:52ZThe at-tier contract review of this PR is on the record as comment Independence pair, machine-readable on the record:
Distinct kinds, so no SELF-REVIEW is reported. Carriers. Neither this PR nor card #18572 carries The three landing preconditions, each measured first-hand at 2026-09-20T14:52Z
Why the file list shrankThe base merge resolved four modify/delete conflicts on The reviewer verified the resolution lost nothing owed, by construction rather than by assertion: the merged tree differs from main parent Reported and not blocking
Flipping to ready and arming the queue on the strength of the readings above. ⛔ No seat approval was sought or given; this PR is not a governed surface, so none is owed. Generated by Claude Code |
…le, not only English (objectstack-ai#19345) Fixes objectstack-ai#17508 Clause-②: no — enumerating a repeater's row children in a `*.form.ts` and adding catalog leaves adds no key to a published payload and moves no accept set; the same author input parses identically before and after. Verified against the diff: no schema file is touched, no `.describe()` or `.meta()` moves, and no row child declares a `type` (so no new form input is offered for anything). ## What this lands Studio renders a `type: 'repeater'` as a table. Its column names come from the form's **declared** row children when the form declares any, and from the served JSON Schema `items.properties[k].title` when it declares none — and `os i18n extract` walks a form field's declared `fields`, so only a **declared** child ever gets a `metadataForms.TYPE.fields['PATH.PROP']` key. objectstack-ai#17232 (PR objectstack-ai#17500) authored the English `.meta({ title })` on thirteen item schemas and objectstack-ai#17505 / objectstack-ai#17506 on four more, but none of them had a catalog channel: every one of those column heads reached a Chinese, Japanese or Spanish author in English. Both halves land together, as the ruling on this card requires: 1. **The form-child enumeration** — 112 row properties across fifteen repeaters, each `label` equal to the item schema's own `.meta({ title })` so the extractor's English source and the schema title stay one string. No `type` on a row child: the row widgets stay schema-derived. 2. **All four catalogs** — `en` (generated from those labels) plus hand-authored `zh-CN` / `ja-JP` / `es-ES` for every leaf. 3. **The pin**, `packages/platform-objects/src/apps/translations/repeater-row-properties.test.ts`, in the shape `dashboard-header-children.test.ts` established for objectstack-ai#17227, **including its last test**: a translated leaf is never a copy of its `en` source. ## The re-derived population The card's figures are a 2026-09-10 reading at `76ddab772f`. Re-derived on this branch's base through the platform's own predicate (`z.toJSONSchema(getMetadataTypeSchema(type), { unrepresentable: 'any', io: 'input' })`, the same instrument `packages/spec/src/kernel/repeater-item-titles.test.ts` uses), at `origin/main` = `e3b3cdd2df3`, 2026-09-20T11:12Z: | | card, 2026-09-10 | measured, 2026-09-20 | |---|---|---| | catalog leaves for full localisation | 604 | **604** (151 row properties x 4) | | leaves belonging to titled properties | 348 | **504** (126 x 4) | | row properties titled | 87 | **126** | | carriers fully titled | 14 | **18** | The total is unchanged; the titled share moved, exactly as the three sibling cards landed since would predict — objectstack-ai#17505 paid `dashboard:widgets` (17) and `dashboard:globalFilters` (10), objectstack-ai#17506 paid `field:options` and `object:fields.options` (6 each, one `SelectOptionSchema`). 87 + 27 + 12 = 126, and 14 + 4 = 18. Of the 504, **12 leaves already existed** (`dashboard:header.actions` from objectstack-ai#17227, and four each on `object:fields.options` and `page:variables`), so **112 keys per locale are new** — 448 new leaves, 336 of them hand-authored translations. Controls on the census, as required: **lit** — `dashboard:header.actions` resolves with exactly `['label','actionUrl','actionType','icon']`, all titled; **dark** — `agent` / `tool` / `hook` / `position` declare no repeater at all and contribute nothing, and a fabricated carrier id resolves to nothing. Both are asserted in the pin, not only measured by hand. ## Translation quality, and how it was checked per locale A glossary was built mechanically from the pre-change catalogs: every `metadataForms.*` and `objects.*` leaf whose translated value differs from its `en` source, indexed by the English string — 1342 distinct English strings with at least one translation. Every new label was then looked up in it, and the catalog's **dominant** existing rendering reused where one existed: - `Label` → 显示名称 / 表示名 / Etiqueta (18 / 19 / 20 existing uses, and the exact values its `object.fields.options.label` twin already carries) - `Name` → 名称 / 名前 / Nombre (30 uses each) · `Type` → 类型 / 型 / Tipo (8) · `Description` → 描述 / 説明 / Descripción (18) - `Filter` → 筛选 / フィルター / Filtro · `Scope` → 范围 / スコープ / Ámbito · `Options` → 选项 / 選択肢 / Opciones - `Timeout (ms)` → the existing 超时(毫秒) / タイムアウト(ms) / Tiempo de espera (ms) · `Input Schema` follows the existing `Output Schema` → 输入 Schema / 入力スキーマ / Esquema de entrada - `field.options.*` mirrors its `object.fields.options.*` twin verbatim, `Color de opción` included — the same `SelectOptionSchema`, so the same words - domain nouns come from the carriers' own already-translated `helpText`: widget 组件 / ウィジェット / widget, dimension 维度 / ディメンション / dimensión, measure 度量 / メジャー / medida, node 节点 / ノード / nodo, edge 连线 / エッジ / conexión, region 区域 / リージョン / región Two deliberate departures, both stated rather than hidden: - **A bare `ID` is qualified by its row**, following the catalog's own `Reference ID` → 引用 ID / 参照 ID / ID de referencia: 节点 ID / ノード ID / ID de nodo. An unqualified `ID` would be byte-identical to its `en` source in all three locales, which the pin's last test refuses — and rightly, since a column head reading `ID` in a Chinese panel is the untranslated state this card exists to end. - **`Dataset` is translated** (数据集 / データセット / Conjunto de datos) although the catalogs carry it untranslated elsewhere. That existing `Dataset` is an `en`-echo — untranslated debt, not a chosen loanword — so this introduces a term for a concept the catalogs do not yet name rather than a second term for one they do. ## The one existing string that moves `page.variables`' children were enumerated (by objectstack-ai#3786's round) **without labels**. The extractor therefore emitted `humanizeFieldPath(path)` as the English source, and `resolveMetadataFormSchemaTitles` wrote that text back over the item schema's authored title: the panel's column head read **`Source`** in every locale while `PageVariableSchema.source` declares `.meta({ title: 'Written By' })`. Two English names for one column, the unauthored one winning. The form now declares the label, so `en` becomes `Written By` and the three translations are re-authored with it (写入组件 / 書き込み元 / Escrito por). That is the **only** pre-existing leaf this PR changes; everything else is additive. The pin's new every-row-child-declares-a-label test is what makes the class impossible to re-enter. ## Fenced out, and seen - `view.columns` / `view.sort` / `view.tabs` — **untouched**. The census surfaces all three (14 + 2 + 9 = 25 untitled row properties), and all three enumerate no children, so they contribute nothing to this diff. Their titling is objectstack-ai#17507's, which is open and unclaimed. The pin asserts `view` is outside its population, so objectstack-ai#17507 will have to add the leaves with the titles — by design. - `retiredKey()` tombstones — **not titled**. The census excludes any row property whose description opens `[REMOVED] `, and `flow.nodes[].outputSchema` is the pinned dark control for that exclusion. - `object.fields.options` keeps its curated four-key subset (`default` and the per-option `visibleWhen` CEL predicate are **not** added as inputs) — that subset is a declared entry in `metadata-form-zod-reconciliation.test.ts`'s ledger, and adding them would widen the offered authoring surface against a recorded decision. ## Verification Base `origin/main` `81e12e186f3` (merged in), head `1cf7f2ae8ca`. Exit codes captured before any pipe throughout. - **Gate families** — `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived **83** commands on this tree; all 83 run with exit codes recorded and reconciled: `83 derived, 83 run, 0 NOT-MEASURED, 0 UNRUN` — "a DERIVED zero — all 83 recorded an exit code and none of them is 3". Two first returned `PREREQUISITE NOT MET` (exit **3**, not a finding): `check:i18n` and `check:dual-build-cjs-loads` both needed built output. Both re-run green after a full build — `check-i18n-bundles: OK (9 package(s) — all bundles in sync, no undeclared authoring keys)`. - **Tests** — `@objectstack/spec`: 501 files / 14655 tests passed, `typecheck` clean. `@objectstack/platform-objects`: 41 files / 583 tests passed, `typecheck` clean. - **Repo-wide lint** — not narrowed: `eslint . --no-inline-config --format json` over eslint's own declared population of **6926** files at `1cf7f2ae8ca`, **0 errors, 0 warnings**, exit 0. - **Ablation — the pin can fail, twice, on both of its load-bearing halves.** Both legs rebuilt and verified against the artifact the suite actually resolves (`@objectstack/spec` is unaliased in `packages/platform-objects/vitest.config.ts`, so the forms come from `dist/`). - *Catalog leg* (no build needed — the catalogs are source to this suite): set `zh-CN` `report.blocks.chart` back to its `en` source. Result: `1 failed | 7 passed`, on `translated locales carry their own text for every leaf, not a copy of the source`. Restored; `git status --porcelain` empty. - *Form leg*: dropped `label: 'Operator'` from `skill.form.ts`'s `triggerConditions` child. Marker `label: "Operator"` in `packages/spec/dist`: **6** files at baseline → **0** after the mutate build → **6** after the restore build. Mutated run `2 failed | 6 passed`, naming `skill:triggerConditions.operator` on both `every enumerated row child declares a label` and `en: each leaf IS the form's declared label`. Restored run `8 passed`, `ablation-dist-preflight` green on presence **and** on a clean tree, source blob back to its `HEAD` hash byte-for-byte. -⚠️ **The first form-leg attempt was a void reading and is reported as such.** Its marker was spelled `label: 'Operator'` (single quotes, as the source has it) while `tsup` emits `label: "Operator"` into `dist` — so the `--absent` pre-flight passed **vacuously**, on a marker that had never been in `dist` at all. The run above is the re-take with the corrected anchor; the numbers quoted are that run's. - **Serial constraints re-taken first-hand** at 2026-09-20T12:17Z, not inherited: the changed-file page of all **35** open PRs, **281** file rows. **No open PR holds any `*.form.ts`, and none holds the translation catalogs.** Firing control: the same map resolves `packages/spec/src/data/filter.zod.ts` to objectstack-ai#19335 and `packages/spec/src/shared/polarity-axes.ts` to objectstack-ai#19318, so it does see `packages/spec/src` holders. Dark control: `packages/spec/src/zzz-no-such.zod.ts` resolves to nothing. ## Acceptance notes Seen while working here, left alone — none is in this card's scope and none is filed: - `scripts/ablation-dist-preflight.mjs`'s `--absent` mode conflates two independent questions in one exit code. Its own header says `--absent` "is two things at once: the mode for a DELETE ablation, and the restore leg of a PLANT one", but its tree limb hard-codes the restore-leg reading — it prints `restore leg: ... 1 path still differs from HEAD` and exits **1** whenever the working tree is dirty. A DELETE ablation's *mutate* leg is necessarily dirty (the mutation is the dirt), so that shape can never pass its own pre-flight, and the failure text instructs the author to restore the very mutation being measured. Reproduced here: a driver that trusted the exit code aborted the ablation at exit 93 with the dist assertion already printed green above it. This is a reproducible defect in an instrument with a named repro, so it is proposed for a card in the report rather than fixed here. - `packages/spec/src/kernel/repeater-item-titles.test.ts`'s header states the bundle overlay "only ever REPLACES a `title` that is already there". `setSchemaTitleAtPath` in `packages/spec/src/system/i18n-resolver.ts` returns `{ ...node, title }` unconditionally, so it also *creates* one. The claim is inert for that file's verdicts (an untitled property with no catalog leaf is untouched either way), so this is a prose inaccuracy, not a defect. - `object.form.ts`'s `fields.options` comment reads "the offered inputs are exactly `SelectOptionSchema`'s authorable keys, minus `visibleWhen`". `default` is also not offered, so the sentence under-counts by one. The behaviour is correct and ledgered as a `subset` in `metadata-form-zod-reconciliation.test.ts`; only the comment's arithmetic is stale. --- _Generated by [Claude Code](https://claude.ai/code/session_01LvwGppdonww4zGLWZo5rho)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #18572
Clause-②: no
⛔ The card body's mechanism is retracted by its own filer (comment 5707586601) and the grading that recited it is superseded by comment 5747478673. There is no alias table; options (i) and (ii) on the body are void. This PR implements the re-graded, suggester-side scope, and every number below was re-derived here rather than carried from the card.
What is actually wrong
findClosestMatchesranks by edit distance and nothing else. On a shape that declares both ends of a range, a key that names neither end is therefore answered with whichever end happens to be spelled more cheaply:endis a three-letter token andstarta five-letter one. That spelling accident is the entire reason the protocol told an author to bind the end of the event. Nobody declared the mapping.And the suggested key is a declared key the runtime honours, so an author who copied the remedy got a document that parses, with the calendar axis silently on the wrong date. Every objectui read site folds
dateFieldontostartDateField. The trap punishes the reader who did what the protocol said.The change
One screen, on the guess only. When the candidate the distance fallback would name carries an axis token the authored key does not, and the shape also declares that candidate's opposite-pole sibling, the rename is replaced by a prescription naming both ends:
dateFieldwas refused before and is refused after; only the sentence the refusal carries changed. This is the fork the card's stop condition names, and it is not taken — nodateField → startDateFieldentry exists anywhere in this diff.aliasesentry is never screened. A human statement about one spelling outranks the guard; only a coin flip is replaced.this fieldkeeps answeringlengthwithmaxLengthexactly as it declares it.field.zod.tsalready writes it by hand forvisible: 「the two answers have opposite polarity … Naming both is the only answer that cannot be acted on wrongly」. What a hand-written entry cannot do is cover the keys nobody enumerated — which is the set a fuzzy suggester answers.Omission vs typo — the condition that keeps it narrow
The guard fires only when the authored key is at least as close to the candidate minus its axis token as to the candidate itself. Without that condition,
axLength— one dropped character inmaxLength, withminLengthdeclared beside it — would lose a perfectly good suggestion. Measured:axLengthvsmaxLengthlength)dateFieldvsendDateFielddatefield)Census — four instances, not one
Measured over 389 registered
strictObjectsurfaces (the audit's own dedup key — surface + alias table + sorted shape keys; the same walk also reads 421 raw registrations and 388 distinct surface strings), by deriving each declared key's axis-silent spelling and asking the real error map what it answers. Four fuzzy instances exist and all four are fixed here:dateFieldendDateFielddateFieldendDateFielddateFieldendDateFieldbaselineFieldbaselineEndFieldTimeline and Gantt are covered, as the dispatch asked. The gantt
baselineStartField/baselineEndFieldrow was found by the census, not by the card.A fifth row the census surfaced is not a defect and is deliberately untouched:
this fieldanswerslengthwithmaxLengthagainst a declaredminLength, and that is a declared alias sitting besidesize: 'maxLength'. It is a decision, so the guard leaves it exactly as written — which is also the precedence pin in the tests.The axis table is judged, not just declared
POLARITY_AXEShas four rows, each attested by a real sibling pair in this package.alias-integrity.test.tsnow fails on a row no surface declares both ends of — the same dead-entry judgement it already applies toaliasesandguidance— with a lit control so an empty verdict is a reading rather than a walk that matched nothing. It is deliberately not a general antonym dictionary.Evidence
Bright control (the premise, re-measured every run). The arithmetic is never written down as
3and5: the test recomputes the distances and asserts the relation — the wrong end inside the budget, the right end outside it — so a rename, a fold change or a budget change reds and names the measurement. A second leg pins that the unguarded ranking still producesendDateFieldon all three surfaces, so the main leg cannot pass for a reason unrelated to the guard.Dark controls.
endField(out of budget at 8 chars, exactly as the filer's control table observed without knowing why) and a nonsense key are asserted byte-identical to each other with only the key name differing — pinning the refusal text, not merely the absence of a hint.Ablation. Guard removed via
scripts/ablation-replace.mjs(anchor hit 1 to 0, blob2fdc252271c2tof1228fa36275), tests re-run, restored withblob == HEADandgit diff HEADempty:The bright, dark, arithmetic and pure-predicate legs stay green under the ablation — they measure different things, and the dark controls really are dark. The new audit was ablated too: adding an unattested axis row reds it by name (
expected [ 'zzleftward/zzrightward' ] to deeply equal []).The declaration-shard diff is controlled, not assumed. Adding a module to the import graph reshuffles TypeScript's declaration-emit order for enum members — 19 declarations across four shards, and the whole diff is three lines (
read,edit,update) changing position. Withpolarity-axes.tsout of the graph andsuggestions.zod.tsrestored to the merge base, a fresh build reportsdeclaration text unchanged (17 entry points, 5364 declarations). The reshuffle is this branch's, so the artifact is regenerated here.Verification
pnpm --filter @objectstack/spec testpnpm --filter @objectstack/spec typecheckpnpm --filter @objectstack/spec buildpnpm --filter @objectstack/spec check:generatedscripts/pm/dispatch-gates.mjs)pnpm lint(repo-wide,eslint . --no-inline-config)9c2725b69dTwo of the 83 are NOT MEASURED, both refusing their own prerequisite with exit 3 rather than reporting a verdict —
check:dual-build-cjs-loads(83 packages with nodist/) andcheck:type-check-debt(27 workspace dependencies unbuilt). Both need a whole-repo build, which isBuild Core's output in CI. ⛔ Neither is a pass and neither is a finding. Reconciliation:83 derived famil(ies) accounted for — 81 run, 2 NOT-MEASURED.Acceptance notes
packages/linthas four directfindClosestMatchescall sites that this guard does not reach, because it lives instrictUnknownKeyErrorand those sites call the ranker directly, over user data field names, with the flat default budget of 3 rather than the length-relative one. Measured against{start_date, end_date, min_amount, max_amount, name}:the_datestill resolves toend_date, whileamountanddatedo not reach the trap (a snake_case name charges the separator too). Same mechanism, second read site, narrow but non-zero reachability. Not fixed here: it addspackages/lintto the affected set and a verification surface this card does not own. Filed for triage rather than silently carried.data/object.zod.ts'ssuggestKeyis a second suggester with the same length-relative budget. Measured:ObjectSchemadeclares 43 top-level keys and zero polarity sibling pairs, so the trap is unreachable there today and nothing guards it if a range pair is ever added. noted, not filed — 承接者: the next card that adds a range pair toObjectSchema.this field'slength: 'maxLength'beside a declaredminLengthreads as an axis collision but is a declared alias, deliberately placed next tosize: 'maxLength'. noted, not filed — 承接者:无, it is a declaration rather than a defect, and the guard's precedence is pinned so it stays one.packages/spec/src/ui/view.zod.tswas not touched — a concurrent card ([finding]FormFieldBaseSchema.scale(view.zod.ts:2768)是第三处无上界的scale—— 撞同一个渲染面天花板,而 #18972 只覆盖了field.zod.ts的两处 #19088) holds it. The calendar, timeline and gantt shapes are read by the tests, never edited.Generated by Claude Code